<!--
 * 电梯导航、联动菜单
 * @作者: 陈老师儿~
 * @手机: 18560000860
 -->
<template>
    <view class="pure-elevator" :class="classes" :style="styles">
        <!-- Tabs -->
        <view class="pure-elevator-tabs">
            <!-- 兼容京东多slot写法 -->
            <slot name="tabs" v-if="$slots.tabs"></slot>
            <pure-tabs :animation="animation" v-bind="tabsOpts" vertical :value="tabValue" @tabClick="handleTabClick" v-else>
                <view class="pure-elevator-tabs-list">
                    <pure-tabs-item v-for="(item, index) in list" :key="index" :value="item[valueKey]" :item="item" :disabled="item.disabled">
                        <text class="pure-elevator-tab">{{ item[nameKey] }}</text>
                    </pure-tabs-item>
                </view>
            </pure-tabs>
        </view>

        <!-- Content -->
        <view class="pure-elevator-right" :id="scrollViewId">
            <scroll-view
                class="pure-elevator-content"
                :scroll-x="false"
                :scroll-y="true"
                :animation="false"
                :scroll-top="bindScrollTop"
                :scroll-with-animation="animation"
                :show-scrollbar="showScrollbar"
                :upper-threshold="upperThreshold"
                :refresher-enabled="refresherEnabled"
                :lower-threshold="lowerThreshold"
                :refresher-threshold="refresherThreshold"
                :refresher-default-style="refresherDefaultStyle"
                :refresher-background="refresherBackground"
                :refresher-triggered="refresherTriggered"
                @scroll="handleScroll"
                @scrolltoupper="handleScrollToUpper"
                @scrolltolower="handleScrollToLower"
                @refresherpulling="handleRefresherPulling"
                @refresherrefresh="handleRefresherRefresh"
                @refresherrestore="handleRefresherStore"
                @refresherabort="handleRefresherAbort"
            >
                <view class="pure-elevator-floors" :id="floorsId">
                    <slot></slot>
                </view>
            </scroll-view>
        </view>
    </view>
</template>

<script>
    import { computed } from "vue";
    import props from "./props.js";
    export default {
        options: {
            // 将自定义节点设置成虚拟的
            // 头条、飞书：因为合并后丢失事件和 'provide/inject'
            // 京东：因为合并后不能合并外层 'class'
            // 百度：因为合并后会在控制台疯狂报警告
            // #ifndef MP-TOUTIAO || MP-LARK || MP-JD || MP-BAIDU
            virtualHost: true,
            // #endif

            // 组件样式隔离
            styleIsolation: "apply-shared",

            // 允许使用多个插槽
            multipleSlots: true,
        },
        props,
        emits: ["tabClick", "tabChange", "subTabChange", "scrollUpper", "scrollLower", "refresherPulling", "refresherRefresh", "refresherRestore", "refresherAbort"],

        data() {
            return {
                // IDs
                scrollViewId: "", // 滚动容器的ID
                floorsId: "", // 楼层容器的ID

                // values
                tabValue: "",
                subTabValue: "",

                // 滚动
                bindScrollTop: 0, // 绑定的滚动距离
                realScrollTop: 0, // 真实的顶部滚动距离，用于判断滚动方向
                scrollDirection: "", // 滚动方向，up:上滑，down:下滑

                // 计算
                isIniting: false, // 是否正在初始化
                isNeedReinit: false, // 是否需要重新初始化
                isCalingTabValue: false, // 是否正在计算 tabValue
                isNeedRecalcTabValue: false, // 是否需要重新计算 tabValue
                isCalingSubTabValue: false,
                isNeedRecalcSubTabValue: false,

                // 楼层数据
                floors: {},

                // 二级分类分组数据
                groups: {},

                // Headers
                headers: {},

                // Footer 节点信息
                footerRect: null,

                // scroll-view 节点信息
                scrollViewRect: null,

                // 调试
                debug: false,
            };
        },
        provide() {
            return {
                addFloor: this.addFloor,
                removeFloor: this.removeFloor,
                addFooter: this.addFooter,
                removeFooter: this.removeFooter,
                addGroup: this.addGroup,
                removeGroup: this.removeGroup,
                addHeader: this.addHeader,
                removeHeader: this.removeHeader,
            };
        },
        expose: ["init"],
        computed: {
            // classes
            classes() {
                const classes = [];
                return classes;
            },

            // styles
            styles() {
                const styles = {};
                return styles;
            },
        },
        watch: {
            // 绑定值变化后
            value: {
                handler(newValue, oldValue) {
                    if (newValue === this.tabValue) return;
                    // 重新初始化
                    this.init();
                },
            },

            // 绑定值变化后
            subValue: {
                handler(newValue, oldValue) {
                    if (newValue === this.subTabValue) return;
                    // 重新初始化
                    this.init();
                },
            },

            // 值变化后提交事件
            tabValue: {
                handler(newValue, oldValue) {
                    // 新值和旧值相同
                    if (newValue === oldValue) return;
                    // 触发事件
                    this.$emit("tabChange", newValue);
                },
            },
        },
        created() {
            // 初始化IDs
            this.initIDs();
        },
        mounted() {
            // 设置 values 默认值
            // this.tabValue = this.value;
            // this.subTabValue = this.subValue;
        },
        methods: {
            // 初始化IDs
            initIDs() {
                this.scrollViewId = `PureElevatorScrollView_${Date.now()}_${this.randomString()}`;
                this.floorsId = `PureElevatorFloors_${Date.now()}_${this.randomString()}`;
            },

            // 初始化
            async init() {
                // 正在初始化
                if (!!this.isIniting) {
                    this.isNeedReinit = true;
                    return;
                }

                // 更新初始化状态
                this.isIniting = true;

                // 等待一下数据更新
                await this.$nextTick();
                await this.sleep(200);

                // 下面这段注释的代码，在快手小程序上会导致右侧不实时滚动
                // 猜测原因：可能是因为快手会重新渲染组件，导致楼层节点数据丢失
                // 下面这个逻辑本来是想节省点性能的，但是在快手小程序上会导致右侧不实时滚动
                // 绑定的 value 与显示的 value 相等，不用更新
                // if (this.value === this.tabValue && this.subValue === this.subTabValue) {
                //     // 更新初始化状态
                //     this.isIniting = false;
                //     // 判断是否需要更新
                //     this.isNeedUpdate();
                //     return false;
                // } else {
                //     this.tabValue = this.value;
                //     this.subTabValue = this.subValue;
                // }

                this.tabValue = this.value;
                this.subTabValue = this.subValue;

                // 获取当前激活项的 item 数据
                let item = null;

                // 有二级分类
                if (Object.keys(this.groups).length > 0) {
                    // 获取二级数据
                    item = this.getActivatedGroupData();
                }
                // 无二级分类
                else {
                    // 获取一级数据
                    item = this.getActivatedFloorData();
                }

                // 未找到激活项
                if (!item) {
                    // 更新初始化状态
                    this.isIniting = false;
                    // 判断是否需要更新
                    this.isNeedUpdate();
                    return false;
                }

                // 滚动到对应内容
                this.scrollToContent(item);
            },

            // 滚动到对应内容
            async scrollToContent(item) {
                // 获取用于计算的节点信息
                // 每次都获取最新的，保证数据的准确性
                const svRect = await this.getScrollViewRect();
                const containerRect = await this.getContainerRect();
                const itemRect = await item.getRect(item);

                // 未成功获取到节点信息
                if (!svRect || !containerRect || !itemRect) {
                    // 更新初始化状态
                    this.isIniting = false;
                    // 判断是否需要更新
                    this.isNeedUpdate();
                    return false;
                }

                // 用于计算的数据
                const containerTop = containerRect.top;
                const itemTop = itemRect.top;

                // 计算滚动距离
                this.bindScrollTop = itemTop - containerTop - this.calcScrollTop(item, svRect);

                // 更新初始化状态
                this.isIniting = false;
                // 判断是否需要更新
                this.isNeedUpdate();
            },

            // 计算滚动到顶部的距离
            // 就是滚动到距离顶部多少像素
            calcScrollTop(item, svRect) {
                // 是否是二级分类内容
                const isSub = item.hasOwnProperty("floorValue");

                // 距离
                let top = isSub ? this.subScrollTop : this.scrollTop;

                // 判断是否是整数
                // 非整数
                if (!Number.isInteger(top)) {
                    // 取计算值
                    top = svRect.height * top;
                }
                return top;
            },

            // 获取一级数据
            getActivatedFloorData() {
                let item = null;
                for (let key in this.floors) {
                    const floor = this.floors[key];
                    if (floor.value === this.tabValue) {
                        item = floor;
                        break;
                    }
                }
                return item;
            },

            // 获取二级数据
            getActivatedGroupData() {
                let item = null;
                for (let key in this.groups) {
                    const group = this.groups[key];
                    if (group.value === this.subTabValue && group.floorValue === this.tabValue) {
                        item = group;
                        break;
                    }
                }
                return item;
            },

            // 获取 scroll-view 节点信息
            getScrollViewRect() {
                return new Promise(async (resolve) => {
                    uni.createSelectorQuery()
                        .in(this)
                        .select(`#${this.scrollViewId}`)
                        .boundingClientRect((rect) => {
                            resolve(rect);
                        })
                        .exec();
                });
            },

            // 获取楼层容器的节点信息
            getContainerRect() {
                return new Promise(async (resolve) => {
                    uni.createSelectorQuery()
                        .in(this)
                        .select(`#${this.floorsId}`)
                        .boundingClientRect((rect) => {
                            resolve(rect);
                        })
                        .exec();
                });
            },

            // 判断是否需要更新
            async isNeedUpdate() {
                // 判断是否需要更新
                if (this.isNeedReinit) {
                    await this.sleep(100);
                    this.isNeedReinit = false;
                    this.init();
                }
            },

            // 添加楼层
            addFloor(id, floor) {
                this.floors[id] = floor;
                // 重新初始化
                this.init();
            },

            // 添加 Footer
            addFooter(rect) {
                this.footerRect = rect;
            },

            // 移除 Footer
            removeFooter() {
                this.footerRect = null;
            },

            // 删除楼层
            removeFloor(id) {
                delete this.floors[id];
                // 重新初始化
                this.init();
            },

            // 添加分组
            addGroup(id, group) {
                this.groups[id] = group;
                // 重新初始化
                this.init();
            },

            // 删除分组
            removeGroup(id) {
                delete this.groups[id];
                // 重新初始化
                this.init();
            },

            // 添加Header
            addHeader(id, header) {
                this.headers[id] = header;
                // 重新初始化
                this.init();
            },

            // 移除Header
            removeHeader(id) {
                delete this.headers[id];
                // 重新初始化
                this.init();
            },

            // 左侧选项卡点击事件
            handleTabClick(item) {
                // 提交点击事件
                this.$emit("tabClick", item);
            },

            // 滚动事件
            handleScroll(e) {
                // 更新滚动距离
                this.realScrollTop = e.detail.scrollTop;

                // 计算左侧一级分类的值
                if (Object.keys(this.groups).length <= 0) this.calcTabsValue();
                // 计算左侧二级分类的值
                else this.calcSubTabsValue();
            },

            // 滚动到顶部/左边
            handleScrollToUpper() {
                this.$emit("scrollUpper");
            },

            // 滚动到底部/右边
            handleScrollToLower() {
                this.$emit("scrollLower");
            },

            // 自定义下拉刷新控件被下拉
            handleRefresherPulling() {
                this.$emit("refresherPulling");
            },

            // 自定义下拉刷新被触发
            handleRefresherRefresh() {
                this.$emit("refresherRefresh");
            },

            // 自定义下拉刷新被复位
            handleRefresherStore() {
                this.$emit("refresherRestore");
            },

            // 自定义下拉刷新被中止
            handleRefresherAbort() {
                this.$emit("refresherAbort");
            },

            // 计算左侧一级分类的值
            async calcTabsValue() {
                // 存储一下，避免重复获取
                let svRect = this.scrollViewRect;
                if (!svRect) {
                    this.scrollViewRect = await this.getScrollViewRect();
                    svRect = this.scrollViewRect;
                }

                // 正在计算
                if (this.isCalingTabValue) {
                    this.isNeedRecalcTabValue = true;
                    return;
                }

                // 更新状态
                this.isCalingTabValue = true;

                // Footer 高度
                const footerHeight = this.footerRect?.height || 0;

                // 总高度
                let totalHeight = footerHeight;
                for (let key in this.floors) {
                    const floor = this.floors[key];
                    const floorHeight = floor?.rect?.height || 0;
                    totalHeight = totalHeight + floorHeight;
                }

                // 楼层距离顶部的距离
                let floorTop = totalHeight - footerHeight;

                // 循环列表
                for (let i = this.list.length - 1; i >= 0; i--) {
                    // 列表项
                    const listItem = this.list[i];
                    const listItemValue = listItem[this.valueKey];

                    // 从 floors 找到对应的项
                    const floorsArray = Object.values(this.floors);
                    const floor = floorsArray.find((_floor) => _floor.value === listItemValue);
                    if (!floor) continue;

                    // 楼层的高度
                    const floorHeight = floor.rect?.height || 0;

                    // 楼层距离顶部的距离
                    floorTop = floorTop - floorHeight;

                    // 触发偏移量
                    const offset = this.calcOffsetTop(floor, svRect);

                    // 判断是否应该被激活
                    // 应该被激活
                    // APP端要有1像素的误差值，不然会出现激活错误的问题
                    if (parseInt(this.realScrollTop) >= parseInt(floorTop - offset) - 1) {
                        // 但是已经滚动到底部了，再更新就会出现无法激活最后几个选项卡的情况
                        // 所以需要判断一下
                        if (parseInt(this.realScrollTop) < parseInt(totalHeight - svRect?.height || 0)) {
                            this.tabValue = listItemValue;
                        } else {
                            // 快手小程序 scroll-view 无法触发相应的事件,模拟该事件
                            // #ifdef MP-KUAISHOU
                            this.handleScrollToLower();
                            // #endif
                        }
                        break;
                    }
                }

                // 别更新的那么频繁
                await this.sleep(50);

                // 更新状态
                this.isCalingTabValue = false;

                // 是否需要重新计算
                // 需要重新计算
                if (this.isNeedRecalcTabValue) {
                    this.isNeedRecalcTabValue = false;
                    this.calcTabsValue();
                }
                // 不需要重新计算
                else {
                    // 清除数据
                    this.scrollViewRect = null;
                }
            },

            // 计算左侧二级分类的值
            async calcSubTabsValue() {
                // 没有二级分类
                if (Object.keys(this.groups).length <= 0) return;

                // 存储一下，避免重复获取
                let svRect = this.scrollViewRect;
                if (!svRect) {
                    this.scrollViewRect = await this.getScrollViewRect();
                    svRect = this.scrollViewRect;
                }

                // 正在计算
                if (this.isCalingSubTabValue) {
                    this.isNeedRecalcSubTabValue = true;
                    return;
                }

                // 更新状态
                this.isCalingSubTabValue = true;

                // Footer 高度
                const footerHeight = this.footerRect?.height || 0;

                // 总高度
                let totalHeight = footerHeight;
                for (let key in this.floors) {
                    const floor = this.floors[key];
                    const floorHeight = floor.rect?.height || 0;
                    totalHeight = totalHeight + floorHeight;
                }

                // 楼层距离顶部的距离
                let floorTop = totalHeight - footerHeight;

                // 循环列表
                outerLoop: for (let i = this.list.length - 1; i >= 0; i--) {
                    // 列表项
                    const listItem = this.list[i];
                    const listItemValue = listItem[this.valueKey];

                    // 从 floors 找到对应的项
                    const floorsArray = Object.values(this.floors);
                    const floor = floorsArray.find((_floor) => _floor.value === listItemValue);
                    if (!floor) continue;

                    // 楼层的高度
                    const floorHeight = floor?.rect?.height || 0;

                    // 楼层Header
                    const headerArray = Object.values(this.headers);
                    const header = headerArray.find((_header) => _header.floorValue === listItemValue);
                    const headerHeight = header?.rect?.height || 0;

                    // Groups
                    const groupsArray = Object.values(this.groups);
                    const floorGroups = groupsArray.filter((_group) => _group.floorValue === listItemValue);

                    // Group 距离顶部的距离
                    let groupTop = floorTop;

                    // 循环Groups
                    for (let j = listItem[this.subListKey]?.length - 1; j >= 0; j--) {
                        const floorGroupValue = listItem[this.subListKey][j][this.subValueKey];
                        const floorGroup = floorGroups.find((_fg) => _fg.value === floorGroupValue && _fg.floorValue === floor.value);
                        if (!floorGroup) continue;

                        // Group Height
                        const floorGroupHeight = floorGroup?.rect?.height || 0;

                        // Group 距离顶部的距离
                        groupTop = groupTop - floorGroupHeight;

                        // 触发偏移量
                        const offset = this.calcOffsetTop(floorGroup, svRect);

                        // 判断是否应该被激活
                        // 应该被激活
                        if (parseInt(this.realScrollTop) >= parseInt(groupTop - offset)) {
                            // 但是已经滚动到底部了，再更新就会出现无法激活最后几个选项卡的情况
                            // 所以需要判断一下
                            if (parseInt(this.realScrollTop) < parseInt(totalHeight - svRect?.height || 0)) {
                                this.tabValue = listItemValue;
                                this.subTabValue = floorGroupValue;
                                this.$emit("tabChange", listItemValue);
                                this.$emit("subTabChange", floorGroupValue);
                            } else {
                                // 快手小程序 scroll-view 无法触发相应的事件,模拟该事件
                                // #ifdef MP-KUAISHOU
                                this.handleScrollToLower();
                                // #endif
                            }
                            break outerLoop;
                        }
                    }

                    // 楼层距离顶部的距离
                    floorTop = floorTop - floorHeight;
                }

                // 别更新的那么频繁
                await this.sleep(50);

                // 更新状态
                this.isCalingSubTabValue = false;

                // 是否需要重新计算
                // 需要重新计算
                if (this.isNeedRecalcSubTabValue) {
                    this.isNeedRecalcSubTabValue = false;
                    this.calcSubTabsValue();
                }
                // 不需要重新计算
                else {
                    // 清除数据
                    this.scrollViewRect = null;
                }
            },

            // 计算偏移量
            // 就是滚动到距离顶部多少像素时就切换Tabs
            calcOffsetTop(item, svRect) {
                // 是否是二级分类内容
                const isSub = item.hasOwnProperty("floorValue");

                // 距离
                let top = isSub ? this.subOffsetTop : this.offsetTop;

                // 判断是否是整数
                // 非整数
                if (!Number.isInteger(top)) {
                    // 取计算值
                    top = svRect.height * top;
                }

                return top;
            },

            // 生成一个36位的随机字符串
            randomString() {
                return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
            },

            // Sleep
            sleep(time = 10) {
                return new Promise((resolve) => {
                    let timer = setTimeout(() => {
                        clearTimeout(timer);
                        resolve();
                    }, time);
                });
            },
        },
    };
</script>

<style lang="scss" scoped>
    @import "./style.css";
</style>
